﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

namespace TypeBuilderFx.Core
{
    /// <summary>Extends a <see cref="MethodBuilder"/> with higher-level methods.</summary>
    /// <remarks>Must call dispose on it when done implementing the method's body.</remarks>
    public class ILGeneratorExtender
    {
        #region Inner Types
        /// <summary>Generic delegate.  It is assumed it will carry code emission.</summary>
        public delegate void InstructionInjector();

        /// <summary>Generic delegate using a variable.  It is assumed it will carry code emission.</summary>
        /// <param name="variable"/>
        public delegate void InstructionInjectorUsingVariable(LocalBuilder variable);

        /// <summary>Generic delegate.  It is assumed it will carry code emission.</summary>
        /// <returns/>
        public delegate LocalBuilder InstructionInjectorReturningVariable();
        #endregion

        private readonly ILGenerator _ilGenerator;
        private readonly IDictionary<string, FieldBuilder> _fieldDictionary;

        /// <summary>Construct an extender by wrapping a <see cref="MethodBuilder"/>.</summary>
        /// <param name="ilGenerator"/>
        /// <param name="fieldDictionary"/>
        public ILGeneratorExtender(ILGenerator ilGenerator, IDictionary<string, FieldBuilder> fieldDictionary)
        {
            _ilGenerator = ilGenerator;
            _fieldDictionary = fieldDictionary;
        }

        /// <summary>Exposes the underlying <see cref="ILGenerator"/>.</summary>
        public ILGenerator ILGenerator
        {
            get { return _ilGenerator; }
        }

        /// <summary>Defines a new label to use in jumping instructions.</summary>
        /// <returns></returns>
        public Label DefineLabel()
        {
            return _ilGenerator.DefineLabel();
        }

        /// <summary>Marks the current point in IL with a label.</summary>
        /// <param name="label"></param>
        public void MarkLabel(Label label)
        {
            if (label == null)
            {
                throw new ArgumentNullException("label");
            }
            _ilGenerator.MarkLabel(label);
        }

        /// <summary>Jumps to a given label.</summary>
        /// <param name="targetLabel"></param>
        public void Goto(Label targetLabel)
        {
            _ilGenerator.Emit(OpCodes.Br, targetLabel);
        }

        /// <summary>Defines a local variable.</summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public LocalBuilder DefineVariable(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }
            LocalBuilder localBuilder = _ilGenerator.DeclareLocal(type);

            return localBuilder;
        }

        /// <summary>Store the current stack value into a local variable.</summary>
        /// <param name="variable"></param>
        public void StoreStackInVariable(LocalBuilder variable)
        {
            if (variable == null)
            {
                throw new ArgumentNullException("variable");
            }
            switch (variable.LocalIndex)
            {
                case 0:
                    _ilGenerator.Emit(OpCodes.Stloc_0);
                    break;
                case 1:
                    _ilGenerator.Emit(OpCodes.Stloc_1);
                    break;
                case 2:
                    _ilGenerator.Emit(OpCodes.Stloc_2);
                    break;
                case 3:
                    _ilGenerator.Emit(OpCodes.Stloc_3);
                    break;
                default:
                    if (variable.LocalIndex <= 0xFF)
                    {
                        _ilGenerator.Emit(OpCodes.Stloc_S, (byte)variable.LocalIndex);
                    }
                    else
                    {
                        _ilGenerator.Emit(OpCodes.Stloc, variable.LocalIndex);
                    }
                    break;
            }
        }

        /// <summary>Put the value (for value type only) of a given local variable on the stack.</summary>
        /// <param name="variable"></param>
        public void StackVariableValue(LocalBuilder variable)
        {
            if (variable == null)
            {
                throw new ArgumentNullException("variable");
            }
            if (!variable.LocalType.IsValueType)
            {
                throw new ArgumentException("A reference type can't stack its value", "variable");
            }
            switch (variable.LocalIndex)
            {
                case 0:
                    _ilGenerator.Emit(OpCodes.Ldloc_0);
                    break;
                case 1:
                    _ilGenerator.Emit(OpCodes.Ldloc_1);
                    break;
                case 2:
                    _ilGenerator.Emit(OpCodes.Ldloc_2);
                    break;
                case 3:
                    _ilGenerator.Emit(OpCodes.Ldloc_3);
                    break;
                default:
                    if (variable.LocalIndex <= 0xFF)
                    {
                        _ilGenerator.Emit(OpCodes.Ldloc_S, (byte)variable.LocalIndex);
                    }
                    else
                    {
                        _ilGenerator.Emit(OpCodes.Ldloc, variable.LocalIndex);
                    }
                    break;
            }
        }

        /// <summary>Put a given local variable reference on the stack (especially useful if it's a value type).</summary>
        /// <param name="variable"></param>
        public void StackVariableReference(LocalBuilder variable)
        {
            if (variable == null)
            {
                throw new ArgumentNullException("variable");
            }
            if (variable.LocalType.IsValueType)
            {
                if (variable.LocalIndex <= 0xFF)
                {
                    _ilGenerator.Emit(OpCodes.Ldloca_S, (byte)variable.LocalIndex);
                }
                else
                {
                    _ilGenerator.Emit(OpCodes.Ldloca, variable.LocalIndex);
                }
            }
            else
            {
                switch (variable.LocalIndex)
                {
                    case 0:
                        _ilGenerator.Emit(OpCodes.Ldloc_0);
                        break;
                    case 1:
                        _ilGenerator.Emit(OpCodes.Ldloc_1);
                        break;
                    case 2:
                        _ilGenerator.Emit(OpCodes.Ldloc_2);
                        break;
                    case 3:
                        _ilGenerator.Emit(OpCodes.Ldloc_3);
                        break;
                    default:
                        if (variable.LocalIndex <= 0xFF)
                        {
                            _ilGenerator.Emit(OpCodes.Ldloc_S, (byte)variable.LocalIndex);
                        }
                        else
                        {
                            _ilGenerator.Emit(OpCodes.Ldloc, variable.LocalIndex);
                        }
                        break;
                }
            }
        }

        /// <summary>
        /// Put a given local variable on the stack (by value for value type, by reference for reference type).
        /// </summary>
        /// <param name="variable"></param>
        public void StackVariable(LocalBuilder variable)
        {
            if (variable == null)
            {
                throw new ArgumentNullException("variable");
            }
            if (variable.LocalType.IsValueType)
            {
                StackVariableValue(variable);
            }
            else
            {
                StackVariableReference(variable);
            }
        }

        /// <summary>Pops the current stack value.</summary>
        /// <remarks>Typically used to get rid of an unused value returned by a method.</remarks>
        public void Pop()
        {
            _ilGenerator.Emit(OpCodes.Pop);
        }

        /// <summary>Returns whatever is on the stack.</summary>
        public void ReturnStack()
        {
            //	Return
            _ilGenerator.Emit(OpCodes.Ret);
        }

        /// <summary>Stacks (or put on the stack) a newed instance.</summary>
        /// <param name="constructorInfo"></param>
        public void StackNew(ConstructorInfo constructorInfo)
        {
            if (constructorInfo == null)
            {
                throw new ArgumentNullException("constructorInfo");
            }
            _ilGenerator.Emit(OpCodes.Newobj, constructorInfo);
        }

        /// <summary>Init the value type currently on the stack.</summary>
        /// <param name="type"></param>
        public void InitValueType(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }
            if (!type.IsValueType)
            {
                throw new ArgumentException(string.Format("'{0}' isn't value type", type.Name), "type");
            }
            _ilGenerator.Emit(OpCodes.Initobj, type);
        }

        /// <summary>Stacks the value of one of the method's parameter.</summary>
        /// <remarks><c>this</c> is the zeroth parameter on a non-static method.</remarks>
        /// <param name="parameterIndex"></param>
        public void StackMethodParameter(int parameterIndex)
        {
            if (parameterIndex < 0)
            {
                throw new ArgumentException("Must be greater than zero.", "parameterIndex");
            }
            switch (parameterIndex)
            {
                case 0:
                    _ilGenerator.Emit(OpCodes.Ldarg_0);
                    break;
                case 1:
                    _ilGenerator.Emit(OpCodes.Ldarg_1);
                    break;
                case 2:
                    _ilGenerator.Emit(OpCodes.Ldarg_2);
                    break;
                case 3:
                    _ilGenerator.Emit(OpCodes.Ldarg_3);
                    break;
                default:
                    if (parameterIndex > 0xFFFF)
                    {
                        throw new ArgumentException("Must be a short integer (less than 0xFFFF)", "parameterIndex");
                    }
                    else
                    {
                        _ilGenerator.Emit(OpCodes.Ldarg_S, parameterIndex);
                    }
                    break;
            }
        }

        /// <summary>Takes the stack value, test if it's an instance of a given type and stacks the boolean result.</summary>
        /// <param name="type"></param>
        public void StackIsInstanceOf(Type type)
        {
            _ilGenerator.Emit(OpCodes.Isinst, type);
        }

        /// <summary>Stacks a <c>null</c>.</summary>
        public void StackNull()
        {
            _ilGenerator.Emit(OpCodes.Ldnull);
        }

        /// <summary>Stacks a <see cref="bool"/> value.</summary>
        /// <param name="value"></param>
        public void StackValue(bool value)
        {
            _ilGenerator.Emit(OpCodes.Ldc_I4, Convert.ToInt32(value));
        }

        /// <summary>Stacks a <see cref="int"/> value.</summary>
        /// <param name="value"></param>
        public void StackValue(int value)
        {
            _ilGenerator.Emit(OpCodes.Ldc_I4, value);
        }

        /// <summary>Stacks a <see cref="double"/> value.</summary>
        /// <param name="value"></param>
        public void StackValue(double value)
        {
            _ilGenerator.Emit(OpCodes.Ldc_R8, value);
        }

        /// <summary>Stacks a <see cref="string"/> value.</summary>
        /// <param name="value"></param>
        public void StackValue(string value)
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentNullException("value");
            }
            _ilGenerator.Emit(OpCodes.Ldstr, value);
        }

        /// <summary>Stacks a non-static field, including the stacking of <c>this</c> beforehand.</summary>
        /// <param name="fieldBuilder"></param>
        public void StackField(FieldBuilder fieldBuilder)
        {
            if (fieldBuilder == null)
            {
                throw new ArgumentNullException("fieldBuilder");
            }
            //  Stacks the 'this' parameter
            StackMethodParameter(0);
            _ilGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
        }

        /// <summary>Stacks a non-static field, using its name, including the stacking of <c>this</c> beforehand.</summary>
        /// <remarks>The field must have been created by <see cref="TypeBuilderExtender.DefinePrivateField"/>.</remarks>
        /// <param name="fieldName"></param>
        public void StackField(string fieldName)
        {
            if (string.IsNullOrEmpty(fieldName))
            {
                throw new ArgumentNullException("fieldName");
            }
            FieldBuilder fieldBuilder;

            if (!_fieldDictionary.TryGetValue(fieldName, out fieldBuilder))
            {
                throw new ApplicationException("Field '{0}' hasn't been defined in TypeBuilderExtender.");
            }

            StackField(fieldBuilder);
        }

        /// <summary>Stores the stack value in a field.</summary>
        /// <param name="fieldBuilder"></param>
        public void StoreStackInField(FieldBuilder fieldBuilder)
        {
            if (fieldBuilder == null)
            {
                throw new ArgumentNullException("fieldBuilder");
            }
            if (fieldBuilder.IsStatic)
            {
                _ilGenerator.Emit(OpCodes.Stsfld, fieldBuilder);
            }
            else
            {
                _ilGenerator.Emit(OpCodes.Stfld, fieldBuilder);
            }
        }

        /// <summary>Stores the stack value in a field.</summary>
        /// <remarks>The field must have been created by <see cref="TypeBuilderExtender.DefinePrivateField"/>.</remarks>
        /// <param name="fieldName"></param>
        public void StoreStackInField(string fieldName)
        {
            FieldBuilder fieldBuilder;

            if (string.IsNullOrEmpty(fieldName))
            {
                throw new ArgumentNullException("fieldName");
            }
            if (!_fieldDictionary.TryGetValue(fieldName, out fieldBuilder))
            {
                throw new ApplicationException("Field '{0}' hasn't been defined in TypeBuilderExtender.");
            }

            StoreStackInField(fieldBuilder);
        }

        /// <summary>Calls a method.</summary>
        /// <param name="constructor"></param>
        public void CallConstructor(ConstructorInfo constructor)
        {
            if (constructor == null)
            {
                throw new ArgumentNullException("constructor");
            }
            _ilGenerator.Emit(OpCodes.Call, constructor);
        }

        /// <summary>Calls a method.</summary>
        /// <param name="method"></param>
        public void CallMethod(MethodInfo method)
        {
            if (method == null)
            {
                throw new ArgumentNullException("method");
            }
            if (method.IsVirtual)
            {
                _ilGenerator.Emit(OpCodes.Callvirt, method);
            }
            else
            {
                _ilGenerator.Emit(OpCodes.Call, method);
            }
        }

        /// <summary>Calls a method with the current method parameters (basically forwarding the method call).</summary>
        /// <param name="method"></param>
        public void CallMethodWithMethodParameters(MethodInfo method)
        {
            if (method == null)
            {
                throw new ArgumentNullException("method");
            }
            int numberOfParametersToPass = method.GetParameters().Length + (method.IsStatic ? 0 : 1);

            for (short i = 0; i != numberOfParametersToPass; ++i)
            {
                StackMethodParameter(i);
            }
            CallMethod(method);
            ReturnStack();
        }

        /// <summary>Casts the stack into another class.</summary>
        /// <param name="targetType"></param>
        public void Cast(Type targetType)
        {
            if (targetType == null)
            {
                throw new ArgumentNullException("targetType");
            }
            if (targetType.IsValueType)
            {
                _ilGenerator.Emit(OpCodes.Unbox_Any, targetType);
            }
            else
            {
                _ilGenerator.Emit(OpCodes.Castclass, targetType);
            }
        }

        /// <summary>Boxes the value on the stack iif the type is value type.</summary>
        public void BoxIfValueType(Type type)
        {
            if (type == null)
            {
                throw new ArgumentNullException("type");
            }
            if (type.IsValueType)
            {
                _ilGenerator.Emit(OpCodes.Box, type);
            }
        }

        /// <summary>If the stack is <c>true</c> the flow keeps going, if not, it jumps to the indicated label.</summary>
        /// <param name="notTrueLabel"></param>
        public void IfTrue(Label notTrueLabel)
        {
            if (notTrueLabel == null)
            {
                throw new ArgumentNullException("notTrueLabel");
            }
            _ilGenerator.Emit(OpCodes.Brtrue, notTrueLabel);
        }

        /// <summary>If the stack is <c>false</c> the flow keeps going, if not, it jumps to the indicated label.</summary>
        /// <param name="notFalseLabel"></param>
        public void IfFalse(Label notFalseLabel)
        {
            if (notFalseLabel == null)
            {
                throw new ArgumentNullException("notFalseLabel");
            }
            _ilGenerator.Emit(OpCodes.Brfalse, notFalseLabel);
        }

        /// <summary>If the stack is <c>null</c> the flow keeps going, if not, it jumps to the indicated label.</summary>
        /// <param name="notNullLabel"></param>
        public void IfNull(Label notNullLabel)
        {
            if (notNullLabel == null)
            {
                throw new ArgumentNullException("notNullLabel");
            }
            _ilGenerator.Emit(OpCodes.Brtrue, notNullLabel);
        }

        /// <summary>If the stack isn't <c>null</c> the flow keeps going, if not, it jumps to the indicated label.</summary>
        /// <param name="notNotNullLabel"></param>
        public void IfNotNull(Label notNotNullLabel)
        {
            if (notNotNullLabel == null)
            {
                throw new ArgumentNullException("notNotNullLabel");
            }
            _ilGenerator.Emit(OpCodes.Brfalse, notNotNullLabel);
        }

        /// <summary>
        /// If the stack is equal to <paramref name="value"/>, the flow keeps going, otherwise, it jumps to the
        /// given label.
        /// </summary>
        /// <param name="value"></param>
        /// <param name="notEqualLabel"></param>
        public void IfEqual(int value, Label notEqualLabel)
        {
            if (value == 0)
            {
                IfFalse(notEqualLabel);
            }
            else
            {
                StackValue(value);
                if (value < 0xFFFF)
                {
                    _ilGenerator.Emit(OpCodes.Beq_S, notEqualLabel);
                }
                else
                {
                    _ilGenerator.Emit(OpCodes.Beq, notEqualLabel);
                }
            }
        }

        /// <summary>
        /// If the stack is equal to <paramref name="value"/>, the flow keeps going, otherwise, it jumps to the
        /// given label.
        /// </summary>
        /// <param name="value"></param>
        /// <param name="notNotEqualLabel"></param>
        public void IfNotEqual(int value, Label notNotEqualLabel)
        {
            if (value == 0)
            {
                IfTrue(notNotEqualLabel);
            }
            else
            {
                StackValue(value);
                if (value < 0xFFFF)
                {
                    _ilGenerator.Emit(OpCodes.Bne_Un_S, notNotEqualLabel);
                }
                else
                {
                    _ilGenerator.Emit(OpCodes.Bne_Un, notNotEqualLabel);
                }
            }
        }

        /// <summary>Throws what is on the stack.</summary>
        public void Throw()
        {
            _ilGenerator.Emit(OpCodes.Throw);
        }

        /// <summary>Validate a given parameter as non-<c>null</c>, throwing a <see cref="ArgumentNullException"/>.</summary>
        /// <param name="parameterIndex"></param>
        /// <param name="parameterName"></param>
        public void ValidateNotNullParameter(int parameterIndex, string parameterName)
        {
            Label afterIfLabel = DefineLabel();

            StackMethodParameter(parameterIndex);

            IfNull(afterIfLabel);
            StackValue(parameterName);
            StackNew(typeof(ArgumentNullException).GetConstructor(new Type[] { typeof(string) }));
            Throw();
            MarkLabel(afterIfLabel);
		}

		/// <summary>Validate a given parameter as non-<c>null</c> or empty, throwing a <see cref="ArgumentNullException"/>.</summary>
		/// <remarks>The parameter must be a string.</remarks>
		/// <param name="parameterIndex"></param>
		/// <param name="parameterName"></param>
		public void ValidateNotNullOrEmptyParameter(int parameterIndex, string parameterName)
		{
			Label afterIfLabel = DefineLabel();

			StackMethodParameter(parameterIndex);
			CallMethod(typeof(string).GetMethod("IsNullOrEmpty"));

			IfFalse(afterIfLabel);
			StackValue(parameterName);
			StackNew(typeof(ArgumentNullException).GetConstructor(new Type[] { typeof(string) }));
			Throw();
			MarkLabel(afterIfLabel);
		}

        #region Try Catch Finally Blocks
        /// <summary>Emits a try/finally block using two delegates to fill the try and finally sub-blocks.</summary>
        /// <param name="tryBodyInjector"></param>
        /// <param name="finallyBodyInjector"></param>
        public void TryFinallyBlock(InstructionInjector tryBodyInjector, InstructionInjector finallyBodyInjector)
        {
            if (tryBodyInjector == null)
            {
                throw new ArgumentNullException("tryBodyInjector");
            }
            if (finallyBodyInjector == null)
            {
                throw new ArgumentNullException("finallyBodyInjector");
            }
            ILGenerator.BeginExceptionBlock();
            tryBodyInjector();
            ILGenerator.BeginFinallyBlock();
            finallyBodyInjector();
            ILGenerator.EndExceptionBlock();
        }

        /// <summary>Emits a using block using a delegate to fill the using sub-blocks.</summary>
        /// <param name="scopeVar"/>
        /// <param name="blockInjector"/>
        public void UsingBlock(LocalBuilder scopeVar, InstructionInjectorUsingVariable blockInjector)
        {
            if (scopeVar == null)
            {
                throw new ArgumentNullException("scopeVar");
            }
            if (blockInjector == null)
            {
                throw new ArgumentNullException("blockInjector");
            }
            //  Call dispose in the finally block
            TryFinallyBlock(
                delegate { blockInjector(scopeVar); },
                delegate
                {
                    StackVariableReference(scopeVar);
                    CallMethod(typeof(IDisposable).GetMethod("Dispose"));
                });
        }

        /// <summary>
        /// Emits a using block using two delegates, one to create the scope variable and another to fill the using
        /// sub-blocks.
        /// </summary>
        /// <param name="variableCreator"/>
        /// <param name="blockInjector"/>
        public void UsingBlock(
            InstructionInjectorReturningVariable variableCreator,
            InstructionInjectorUsingVariable blockInjector)
        {
            if (variableCreator == null)
            {
                throw new ArgumentNullException("variableCreator");
            }
            if (blockInjector == null)
            {
                throw new ArgumentNullException("blockInjector");
            }
            //  Call dispose in the finally block
            LocalBuilder scopeVariable = variableCreator();

            UsingBlock(scopeVariable, blockInjector);
        }
        #endregion

        #region Loop Blocks
        /// <summary>Emits a while loop using a boolean test and a while body.</summary>
        /// <param name="prepareWhileTestInjector"></param>
        /// <param name="whileBodyInjector"></param>
        public void While(InstructionInjector prepareWhileTestInjector, InstructionInjector whileBodyInjector)
        {
            if (prepareWhileTestInjector == null)
            {
                throw new ArgumentNullException("prepareWhileTestInjector");
            }
            if (whileBodyInjector == null)
            {
                throw new ArgumentNullException("whileBodyInjector");
            }

            Label loopStartLabel = DefineLabel();
            Label testConditionLabel = DefineLabel();

            Goto(testConditionLabel);
            MarkLabel(loopStartLabel);
            whileBodyInjector();
            MarkLabel(testConditionLabel);
            prepareWhileTestInjector();
            IfTrue(loopStartLabel);
        }
        #endregion
    }
}